#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#

import time
import random

try :
    from hashlib import md5
except :
    from md5 import md5


from callinlistener import CallInListener

from id import ID
from url import URL
from interval import Interval
from tcpproxy import TcpProxy
from passbyproxy import PassByProxy

from help import logging

from consts import STATUS_OK,EBIN_NOT_READY


class   Node(CallInListener) :

    FINGER_TABLE_SIZE = ID.LENGTH
    FINGER_DISTANCES = (1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648, 4294967296, 8589934592, 17179869184, 34359738368, 68719476736, 137438953472, 274877906944, 549755813888, 1099511627776, 2199023255552, 4398046511104, 8796093022208, 17592186044416, 35184372088832, 70368744177664, 140737488355328, 281474976710656 )
    FINGER_DISTANCES = (0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000, 0x800000, 0x1000000, 0x2000000, 0x4000000, 0x8000000, 0x10000000, 0x20000000, 0x40000000, 0x80000000, 0x100000000, 0x200000000, 0x400000000, 0x800000000, 0x1000000000, 0x2000000000, 0x4000000000, 0x8000000000, 0x10000000000, 0x20000000000, 0x40000000000, 0x80000000000, 0x100000000000, 0x200000000000, 0x400000000000, 0x800000000000, 0x1000000000000, 0x2000000000000, 0x4000000000000 )

    REPLICA_FACTOR_DEFAULT = 3
    
    class   FingerTableEntry(object) :
        def __init__(self, node=None, id=None, interval=None) :
            # proxy to remote Node who charge the finger range
            self.node = node
            # ID of remote Node who charge the finger range
            self.id = id
            # ID interval to charge by the remote Node
            self.interval = interval
            ## __init__()

        def __str__(self) :
            id = None
            if self.id :
                id = self.id.value
            url = None
            if self.node :
                url = self.node.url.getURL()
            return '<Finger Interval[%d .. %d) Successor(ID:%s URL:%s)>' % (self.interval.left, self.interval.right, str(id), str(url))
            ## __str__()

        ## class FingerTableEntry

    class   MD5Hash(object) :
        def __init__(self) :
            self.bit_count = ID.LENGTH
            cut_size,rshift_size = self.__getCutAndShiftSize( self.bit_count )
            self.cut_size = cut_size
            self.rshift_size = rshift_size
            ## __init__()

        def __getCutAndShiftSize(self, bit_count) :
            r = bit_count % 4 
            cut_size = bit_count / 4 
            if r : cut_size += 1
            rshift_size = cut_size * 4 - bit_count
            return cut_size, rshift_size
            ## __getCutAndShiftSize()

        def __call__(self, str) :
            '''calculate hash using md5 algorithm

            @param str key to be calculated (string)

            @return x hash value (int)
            '''
            m = md5(str).hexdigest()
            x = int(m[ :self.cut_size ], 16) >> self.rshift_size
            return x
            ## __call__()

        ## class MD5Hash

    def __init__(self, url, id) :
        # maybe CallInListener as a reference, NOT an Interface ??? 
        self.url = url
        self.id = id
        self.id_n = id.value

        self.predecessor = None
        self.finger_table = {}
        for i in range(self.FINGER_TABLE_SIZE) :
            interval = Interval()
            interval.left  = (self.id + self.__class__.FINGER_DISTANCES[ i ]) % ID.MAXIMUM
            interval.right = (self.id + self.__class__.FINGER_DISTANCES[i + 1]) % ID.MAXIMUM
            self.finger_table[ i ] = self.__class__.FingerTableEntry(node=None, id=None, interval=interval)

        self.successor_list = []

        self.proxy_to_myself = PassByProxy(self.url, self)

        self.when_finger_last_update = 0
        self.when_finger_last_check = 0

        self.hash = self.__class__.MD5Hash()
        self.node_bin = None

        # about replica
        self.setReplicaFactor( self.REPLICA_FACTOR_DEFAULT )

        self.logger = logging.getLogger('OpenChord')
        ## __init__()
    
    def __del__(self) :
        for i in range(self.FINGER_TABLE_SIZE) :
            self.finger_table[ i ] = None
        self.finger_table = {}
        self.predecessor = None

        self.proxy_to_myself = None
        ## __del__()

    def __str__(self) :
        return '<Node ID=%d URL=%s>' % (self.id_n, self.url.getURL())
        ## __str__()

    def getID(self) :
        return  self.id
        ## getID()

    def setFinger(self, index, id, finger_url) :
        if index < 0 or index >= FINGER_TABLE_SIZE :
            raise IndexError, 'index #%d is out of finger table size (%d)' % (index, self.FINGER_TABLE_SIZE)
        self.finger_table[ index ].id = id
        self.finger_table[ index ].node = TcpProxy( URL( finger_url ) )

        self.when_finger_last_update = time.time()
        ## setFinger()

    def getFinger(self, index) :
        if index < 0 or index >= FINGER_TABLE_SIZE :
            raise IndexError, 'index #%d is out of finger table size (%d)' % (index, self.FINGER_TABLE_SIZE)
        return self.finger_table[ index ]
        ## getFinger()

    def setSuccessor(self, successor) :
        self.finger_table[0].node = successor
        self.finger_table[0].id = successor.getID()

        self.when_finger_last_update = time.time()
        ## setSuccessor()

    def getSuccessor(self) :
        return self.finger_table[0].node
        ## getSuccessor()

    def setPredecessor(self, predecessor) :
        #if isinstance(predecessor.url, str) :
        #    raise RuntimeError

        self.predecessor = predecessor

        self.when_finger_last_update = time.time()
        ## setPredecessor()

    def getPredecessor(self) :
        return  self.predecessor
        ## getPredecessor()

    #### --------------------------------------------------------- ####

    def findSuccessor(self, id) :
        predecessor = self.findPredecessor( id )
        successor = predecessor.getSuccessor()

        if self.logger.isDebugEnabled() :
            successor_id = successor.getID().value
            self.logger.debug('Node#%d findSuccessor(%d) get (ID:%d)',self.id_n, id.value,successor_id)

        self.addSuccessor(successor)
        return successor
        ## findSuccessor()

    def findPredecessor(self, id) :
        proxy = self.proxy_to_myself
        n_id = proxy.getID()
        n_successor_id = proxy.getSuccessor().getID()
        while not id.isInOpenClosedInterval(n_id, n_successor_id) :
            # id in [n_id, n_successor_id)
            if id == n_successor_id :
                break

            self.logger.debug('Node#%d findPredecessor %s is NOT-IN ( %s, %s ]', self.id_n, id.value, n_id.value,n_successor_id.value)
            _proxy = proxy.findClosestPrecedingFinger(id)
            if not _proxy :
                break
            if proxy == _proxy : 
                ## would loop recursively
                break

            proxy = _proxy
            n_id = proxy.getID()
            n_successor_id = proxy.getSuccessor().getID()

        self.logger.debug('Node#%d findPredecessor(%d) get (ID:%d)',self.id_n,id.value,n_id.value)
        return proxy
        ## findPredecessor()

    def findClosestPrecedingFinger(self, id) :
        '''find closest finger preceding node

        @return proxy to remote node
        '''
        for i in range(self.FINGER_TABLE_SIZE - 1, -1, -1) :
            ##finger_id = ID( self.id + self.__class__.FINGER_DISTANCES[ i ] )
            finger_id = self.finger_table[ i ].id
            if not finger_id :
                # the finger is NULL
                next
            proxy = self.finger_table[ i ].node
            if finger_id.isInInterval(self.id, id) :
                if self.logger.isTraceEnabled() :
                    self.logger.trace('Node#%d findClosestPrecedingFinger(%d) get N#%d on finger#%d', self.id_n, id.value, proxy.getID().value, i)
                return proxy

        return self.proxy_to_myself
        ## findClosestPrecedingFinger()

    def joinSelf(self) :
        for i in range(self.FINGER_TABLE_SIZE) :
            fte = self.finger_table[ i ]
            fte.node = self.proxy_to_myself
            fte.id = self.id
        self.predecessor = self.proxy_to_myself

        return True
        ## joinSelf()

    def join(self, bootstrap_node) :
        '''
        @return True on success, False on failure
        '''
        proxy = bootstrap_node

        self.logger.info('Node#%d join (bootstrap Node#%d) ...', self.id_n, proxy.getID().value)
        self.initFingerTable(proxy)
        self.updateOtherNodes()

        return True
        ## join()

    def initFingerTable(self, proxy) :
        successor_id = ID(self.id + 1)
        successor = proxy.findSuccessor(successor_id)
        if not successor :
            self.logger.error('fail to find successor of "%s"', str(successor_id))
            return None
        self.setSuccessor( successor )

        predecessor = successor.getPredecessor()
        if not predecessor :
            self.logger.error('fail to find predecessor of "%s"', successor)
            return None
        self.setPredecessor( predecessor )
        
        successor.setPredecessor( self.proxy_to_myself )

        for i in range(self.FINGER_TABLE_SIZE - 1) :
            if self.logger.isTraceEnabled() : self.logger.trace('initFingerTable BEFORE ---- : %s', self.toPrettyString())
            fte = self.finger_table[ i ]
            fte_succ = self.finger_table[ i + 1 ]
            if fte_succ.id and fte.id.isInClosedOpenInterval(self.id, fte.id) :
                fte_succ.node = fte.node
                fte_succ.id = fte.id
                self.logger.info('Node#%d set %d-th finger to (ID:%s URL:%s)', self.id_n, i+1, str(fte_succ.id.value), fte_succ.node.url.getURL())
            else :
                fte_succ.node = proxy.findSuccessor(fte_succ.interval.left)
                fte_succ.id = fte_succ.node.getID()
                self.logger.info('Node#%d set %d-th finger to (ID:%s URL:%s)', self.id_n, i+1, str(fte_succ.id.value), fte_succ.node.url.getURL())
            if self.logger.isTraceEnabled() : self.logger.trace('initFingerTable AFTER ++++ : %s', self.toPrettyString())
        # TODO 
        ## initFingerTable()

    def updateOtherNodes(self) :
        n = self.id.value
        node_proxy = self.proxy_to_myself
        for i in range(self.FINGER_TABLE_SIZE) :
            # find last node p whose i-th finger might be n (this node)
            p_id = (n + ID.MAXIMUM - self.FINGER_DISTANCES[ i ]) % ID.MAXIMUM
            predecessor_id = ID( p_id )
            p = self.findPredecessor( predecessor_id )
            if self.logger.isDebugEnabled() :
                _p_id = p.getID().value
                self.logger.debug('Node#%d will update N#%d\'s %d-th finger to N#%d', self.id_n, _p_id, i, self.id_n)
            p.updateFingerTable( node_proxy, i )
        ## updateOtherNodes()

    def updateFingerTable(self, s, i) :
        '''
        if s is i-th finger of n, update n's finger table with s
        '''
        s_id = s.getID()
        fte_i = self.finger_table[ i ]

        modified = False
        if self.id == fte_i.id :
            # does this mean this is a self-ring ???
            fte_i.node = s
            fte_i.id = s_id

            modified = True
            self.logger.info('Node#%d update %d-th finger to N#%d', self.id_n, i, fte_i.id.value)

        if s_id.isInClosedOpenInterval(self.id, fte_i.id) :
            #self.logger.debug('updateFingerTable BEFORE ---- : %s', self.toPrettyString())
            self.logger.debug('Node#%d update FingerTable: %d in [%d, %d)', self.id_n, s_id.value, self.id_n, fte_i.id.value)
            fte_i.node = s
            fte_i.id = s_id
            self.logger.info('Node#%d update %d-th finger to N#%d', self.id_n, i, fte_i.id.value)
            #self.logger.debug('updateFingerTable AFTER  ++++ : %s', self.toPrettyString())

            # get first node preceding n
            p = self.predecessor
            if self.logger.isDebugEnabled() :
                _p_id = p.getID().value
                self.logger.debug('Node#%d will update Predecessor N#%d\'s %d-th finger to N#%d', self.id_n, _p_id, i, s_id.value)
            p.updateFingerTable(s, i)

            modified = True

        self.addSuccessor( s )
        if modified : self.when_finger_last_update = time.time()
        ## updateFingerTable()

    #### --------------------------------------------------------- ####

    def addSuccessor(self, successor) :
        # must be not empty
        if not successor : return
        # must not myself
        s_id = successor.getID()
        if s_id == self.id : return

        if len(self.successor_list) >= self.FINGER_TABLE_SIZE :
            the_last_id = self.successor_list[ -1 ].getID()
            if s_id.isInInterval(the_last_id, self.id) :
                return
          
        inserted = False
        for i in range(len(self.successor_list)) :
            the_id = self.successor_list[ i ].getID()
            if s_id == the_id : 
                # already in successor_list
                return
            if s_id.isInInterval(self.id, the_id) :
                self.successor_list.insert(i, successor)
                inserted = True
                break

        if not inserted :
            # just append to successor_list
            self.successor_list.append(successor)
            inserted = True

        if len(self.successor_list) >= self.FINGER_TABLE_SIZE :
            # there are too many successor in list, try to clear some
            pass

        self.when_finger_last_update = time.time()
        ## addSuccessor()

    def removeSuccessor(self, successor) :
        for i in range(len(self.successor_list)) :
            the_id = self.successor_list[ i ].getID()
            if s_id == the_id : 
                # already in successor_list
                self.successor_list.pop(i)
                break

        self.when_finger_last_update = time.time()
        ## removeSuccessor()

    #### --------------------------------------------------------- ####

    def stablize(self) :
        successor = self.getSuccessor()
        x = successor.getPredecessor()
        s_id = successor.getID()
        x_id = x.getID()
        if x_id.isInInterval(self.id, s_id) :
            # set new Successor
            self.logger.info('stablize update successor N#%d --> N#%d',s_id.value,x_id.value)
            self.setSuccessor( x )
            successor = self.getSuccessor()

        successor.claimPredecessor(self.proxy_to_myself)
        ## stablize()

    def claimPredecessor(self, potential_predecessor) :
        p_current = self.getPredecessor()
        if not p_current :
            self.setPredecessor(potential_predecessor)
        else :
            p_current_id = p_current.getID()
            p_new_id = potential_predecessor.getID()
            if p_new_id.isInInterval(p_current_id, self.id) :
                self.setPredecessor(potential_predecessor)
        ## claimPredecessor()

    def fixFingers(self) :
        i = random.randint(0, self.FINGER_TABLE_SIZE - 1)
        fte = self.finger_table[ i ]
        finger_old = fte.node
        id_old = fte.id

        finger = self.findSuccessor( ID(fte.interval.left) )
        fte.node = finger
        fte.id = finger.getID()

        if self.logger.isDebugEnabled() :
            id_n = fte.id.value
            if id_old :
                id_old_str = 'N#%d' % id_old.value
            else :
                id_old_str = 'None'
            self.logger.debug('Node#%d fix #%d-th finger %s --> N#%d', self.id_n, i, id_old_str, id_n)

        if finger :
            # delete old finger
            finger_old = None
        ## fixFingers()

    #### --------------------------------------------------------- ####

    def lookup(self, key_id) :
        '''lookup route info by key_id

        @param key_id ID of key

        @return 3-tuple(status, Interval, host_node, replica_node_list)
        
        status : 
            200 -- ok, the following Interval, host_node, replica_node_list is valid
            400 -- not found, (maybe chord ring is stablizing)
        '''
        if isinstance(key_id, int) :
            key_id = ID(key_id)
        successor = self.findSuccessor(key_id)
        interval = successor.getInterval()
        host_node = successor
        replica_node_list = successor.getReplicas()

        return (200, interval, host_node, replica_node_list)
        ## lookup()

    def getInterval(self) :
        predecessor_id = self.predecessor.getID()

        return (predecessor_id, self.id)
        ## getInterval()

    def getReplicas(self) :
        replicas = self.successor_list[ :self.replica_node_count ]

        return replicas
        ## getReplicas()

    #### --------------------------------------------------------- ####

    def put(self, key, data, timestamp=0) :
        '''
        @param key key of record
        @param data data of record
        @param timestamp timestamp of record 

        @return 2-tuple(status, key)
        status :
            200 -- ok 
        '''
        key_id = self.hash(key)

        status,interval,host_node,replica_node_list = self.lookup(key_id)
        if status != 200 :
            return status,key

        if not timestamp :
            timestamp = time.time()
        if isinstance(timestamp, float) :
            timestamp = int(timestamp * 1000)

        op_status,op_keyid,op_key = host_node.insert(key_id, key, data, timestamp)
        if op_status != 200 :
            return op_status,key

        # TODO : to replicate the data
        ## put()

    def get(self, key, timestamp=0) :
        '''
        @param key key of record
        @param timestamp timestamp of record

        @return 4-tuple(status, key, data, timestamp)
        '''
        key_id = self.hash(key)

        status,interval,host_node,replica_node_list = self.lookup(key_id)
        if status != 200 :
            return status,key,'',0

        if not timestamp :
            timestamp = time.time()
        if isinstance(timestamp, float) :
            timestamp = int(timestamp * 1000)

        op_status,op_keyid,op_key = host_node.retrieve(key_id, key, timestamp)
        if op_status != 200 :
            return op_status,key,'',0

        # TODO : to read-repair
        ## get()

    def delete(self, key, timestamp=0) :
        '''
        @param key key of record
        @param timestamp timestamp of record

        @return 2-tuple(status, key)
        '''
        key_id = self.hash(key)

        status,interval,host_node,replica_node_list = self.lookup(key_id)
        if status != 200 :
            return status,key

        if not timestamp :
            timestamp = time.time()
        if isinstance(timestamp, float) :
            timestamp = int(timestamp * 1000)

        op_status,op_keyid,op_key = host_node.remove(key_id, key, timestamp)
        if op_status != 200 :
            return op_status,key

        # TODO : to remove replica
        ## del()

    #### --------------------------------------------------------- ####

    def insert(self, key_id, key, data, timestamp) :
        '''insert key,data into local node-bin

        @return 3-tuple(status,key_id, key)
        status :
            EBIN_NOT_READY   -- node-bin is not ready
        '''
        if not self.node_bin :
            return EBIN_NOT_READY,key_id,key

        status,key_id,key = self.node_bin.put(key_id, key, data, timestamp)

        return status,key_id,key
        ## insert()

    def retrieve(self, key_id, key, timestamp) :
        '''retrieve data from local node-bin

        @return 5-tuple(status,key_id,key,data,timestamp)
        status :
            EBIN_NOT_READY   -- node-bin is not ready
        '''
        if not self.node_bin :
            return EBIN_NOT_READY,key_id,key,'',0

        status,key_id,key,data,ts = self.node_bin.get(key_id, key, timestamp)

        return status,key_id,key,data,ts
        ## retrieve()

    def remove(self, key_id, key, timestamp) :
        '''
        @return 3-tuple(status,key_id,key)
        status :
            EBIN_NOT_READY   -- node-bin is not ready
        '''
        if not self.node_bin :
            return EBIN_NOT_READY,key_id,key

        status,key_id,key = self.node_bin.remove(key_id, key, timestamp)

        return status,key_id,key
        ## remove()

    #### --------------------------------------------------------- ####

    def setHash(self, hash) :
        if not callable(hash) :
            raise TypeError, 'Hash functor must be callable.'

        self.hash = hash
        ## setHash()

    def setNodeBin(self, bin) :
        self.node_bin = bin
        ## setNodeBin()

    #### --------------------------------------------------------- ####

    def callin(self, method, parameters) :
        response = None
        if 'findSuccessor' == method :
            id = ID(parameters)
            successor = self.findSuccessor(id)
            self.logger.trace('CALLIN method:%s successor:%s', method, successor)
            response = successor.url.getURL()
        elif 'getSuccessor' == method :
            successor = self.getSuccessor()
            self.logger.trace('CALLIN method:%s successor:%s', method, successor)
            response = successor.url.getURL()
        elif 'getPredecessor' == method :
            predecessor = self.getPredecessor()
            self.logger.trace('CALLIN method:%s predecessor:%s', method, predecessor)
            response = predecessor.url.getURL()
        elif 'setPredecessor' == method :
            url_predecessor = parameters
            url = URL( url_predecessor )
            if self.url.getURL() == url_predecessor :
                predecessor = PassByProxy( url, self )
            else :
                predecessor = TcpProxy( url )
            self.setPredecessor(predecessor)
            self.logger.trace('CALLIN method:%s predecessor:%s', method, url_predecessor)
        elif 'getID' == method :
            id = self.getID()
            self.logger.trace('CALLIN method:%s id:%s', method, id)
            response = id.value
        elif 'findClosestPrecedingFinger' == method :
            id = ID(parameters)
            finger = self.findClosestPrecedingFinger(id)
            self.logger.trace('CALLIN method:%s finger:%s', method, finger)
            response = finger.url.getURL()
        elif 'updateFingerTable' == method :
            url_proxy, index = parameters
            url = URL( url_proxy )
            if self.url.getURL() == url_proxy :
                proxy = PassByProxy( url, self )
            else :
                proxy = TcpProxy( url )
            self.logger.trace('CALLIN %s(%s, %d)', method, url_proxy, index)
            self.updateFingerTable( proxy, index )
        elif 'claimPredecessor' == method :
            url_predecessor = parameters
            url = URL( url_predecessor )
            if self.url.getURL() == url_predecessor :
                predecessor = PassByProxy( url, self )
            else :
                predecessor = TcpProxy( url )
            self.claimPredecessor(predecessor)
            self.logger.trace('CALLIN method:%s predecessor:%s', method, url_predecessor)
        elif 'getInterval' == method :
            p_id,l_id = self.getInterval()
            self.logger.trace('CALLIN method:%s interval(%s, %s)', method, str(p_id.value), str(l_id.value))
            response = (p_id.value, l_id.value)
        elif 'getReplicas' == method :
            replicas = self.getReplicas()
            replicas_url = []
            for replica in replicas :
                replicas_url.append( replica.url.getURL() )
            self.logger.trace('CALLIN method:%s replicas:%s', method, replicas_url)
            response = replicas_url
        elif 'insert' == method :
            key_id,key,data,timestamp = parameters
            op_status,op_key_id,op_key = self.insert(key_id,key,data,timestamp)
            response = (op_status, op_key_id, op_key)
            self.logger.debug('CALLIN method:%s parameters:%s response:%s', method, parameters, response)
        elif 'retrieve' == method :
            key_id,key,timestamp = parameters
            op_status,op_key_id,op_key,op_timestamp = self.insert(key_id,key,timestamp)
            response = (op_status, op_key_id, op_key, op_data, op_timestamp)
        elif 'remove' == method :
            key_id,key,timestamp = parameters
            op_status,op_key_id,op_key = self.remove(key_id,key,timestamp)
            response = (op_status, op_key_id, op_key)
        else :
            raise NotImplementedError,'method:%s parameters:%s' % (method, parameters)

        return response
        ## callin()

    #### --------------------------------------------------------- ####

    def setReplicaFactor(self, factor) :
        '''set number of copies of each record

        @param factor number of replica copies for each record 
        '''
        self.replica_factor = factor
        self.replica_node_count = factor - 1

        # number of replica must finish successfully before writing operation done.
        self.replica_write_done_count = self.replica_node_count / 2
        ## setReplicaFactor()

    #### --------------------------------------------------------- ####

    def toPrettyString(self) :
        result = []
        result.append( 'Chord Node#%d URL=%s' % (self.id.value, self.url.getURL()) )
        if self.predecessor and self.predecessor.url :
            predecessor_url = self.predecessor.url.getURL()
        else :
            predecessor_url = 'None'
        result.append( '  Predecessor=%s' % predecessor_url)

        successors = []
        successors.append('  Successors :')
        for s in self.successor_list :
            successors.append( 'N#%d' % s.getID().value )
        result.append( ' '.join(successors) )

        result.append( '  FingerTable (m=%d) :' % self.FINGER_TABLE_SIZE )
        RANGE = self.FINGER_DISTANCES[ self.FINGER_TABLE_SIZE ]
        for i in range(self.FINGER_TABLE_SIZE) :
            fte = self.finger_table[ i ]
            proxy = fte.node
            if proxy :
                proxy_url = proxy.url.getURL()
            else :
                proxy_url = 'None'
            result.append( '    FTE#%-2d %s' % (i, str(fte))  )
            #result.append( '    FTE#%-2d %s [%d %d) %s' % (i, str(fte.id), fte.interval.left, fte.interval.right, proxy_url)  )
        return '\n'.join(result)
        ## toPrettyString()

    def getPrettyPrintFingerTable(self) : 
        return self.toPrettyString()
        ## getPrettyPrintFingerTable()

    ## class Node

